/*
 * Copyright (c) 2003-2008 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jmex.swt.lwjgl;

import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.opengl.GLCanvas;
import org.eclipse.swt.opengl.GLData;
import org.eclipse.swt.widgets.Composite;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ARBMultisample;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GLContext;
import org.monklypse.core.JMECanvasImplementor2;

import com.jme.input.InputSystem;
import com.jme.renderer.lwjgl.LWJGLRenderer;
import com.jme.system.DisplaySystem;
import com.jme.system.canvas.JMECanvas;
import com.jme.system.canvas.JMECanvasImplementor;
import com.jme.system.lwjgl.LWJGLDisplaySystem;
import com.jmex.swt.lwjgl.LWJGLSWTConstants.Threadmode;

/**
 * <code>LWJGLCanvas</code>
 * 
 * @author Joshua Slack
 * @author Jochen Mader
 */
public class LWJGLSWTCanvas extends GLCanvas implements JMECanvas {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = Logger.getLogger(LWJGLSWTCanvas.class
			.getName());

	private static final long serialVersionUID = 1L;
	/**
	 * Implementor for the basic funtionality.
	 */
	private JMECanvasImplementor impl;
	/**
	 * Used to decide wether the input should be updated or not.
	 */
	private boolean updateInput = false;

	/**
	 * Milliseconds per frame.
	 */
	private int syncMS = 0;

	/**
	 * Milliseconds between two updates.
	 */
	private int updateMS = 0;
	/**
	 * Frames per second.
	 */
	private int syncRate = 0;

	private boolean drawWhenDirty = false;

	private boolean dirty = true;

	private static boolean initialized = false;
	
    private boolean shouldAutoKillContext = true;
	
	/**
	 * Thread for rendering.
	 */
	private static RenderThread renderThread;
	/**
	 * Thread for updating the scene and input.
	 */
	private static UpdateThread updateThread;
	/**
	 * Thread behaviour.
	 */
	private boolean single;
	/**
	 * If two threads are running this is the mutex to use.
	 */
	private Semaphore mutex;
	/**
	 * Timestamp for the next render run.
	 */
	private long nextrender = 0;
	/**
	 * Timestamp for the next update run.
	 */
	private long nextupdate = 0;

	private LWJGLRenderer renderer = null;

	/**
	 * Constructor. Thread mode defaults to single thread.
	 * 
	 * @param parent
	 * @param style
	 * @param data
	 * @throws LWJGLException
	 */
	public LWJGLSWTCanvas(Composite parent, int style, GLData data)
			throws LWJGLException {
		this(parent, style, data, Threadmode.SINGLE);
	}

	/**
	 * Constructor that allows picking of thread mode.
	 * 
	 * @param parent
	 * @param style
	 * @param data
	 * @param threadmode
	 * @throws LWJGLException
	 */
	public LWJGLSWTCanvas(Composite parent, int style, GLData data,
			Threadmode threadmode) throws LWJGLException {
		super(parent, style, data);
		// this check is save as all actions in eclipse occur in one thread
		if (!initialized) {
			if (Threadmode.MULTI.equals(threadmode)) {
				mutex = new Semaphore(1);
				renderThread = new RenderThread("renderThread", mutex,
						getDisplay());
				updateThread = new UpdateThread("renderThread", mutex);
				single = false;
			} else {
				renderThread = new RenderThread("renderThread", null,
						getDisplay());
				single = true;
			}
			initialized = true;
		}
		renderThread.addTarget(this);
		if (Threadmode.MULTI.equals(threadmode)) {
			updateThread.addTarget(this);
		}
	}

	@Override
	public void dispose() {
	    if (shouldAutoKillContext) {
	        initialized = false;
	        super.dispose();
	    }
	}

	/**
	 * Set the implementor for this canvas.
	 * 
	 * @param impl
	 *            the implementor to set
	 */
	public void setImplementor(JMECanvasImplementor impl) {
		this.impl = impl;
		init();
		if (!renderThread.isAlive()) {
			if (!single) {
				updateThread.start();
			}
			renderThread.start();
		}
	}

	/**
	 * Init the system.
	 */
	@SuppressWarnings("unchecked")
	public void init() {
        if (initialized) {
            // The throwable is just so we have a stack trace available in the log.
            logger.log(Level.WARNING, "init() called when already initialized.", new Throwable());
        }
        initialized = true;
		try {
			LWJGLDisplaySystem display = (LWJGLDisplaySystem) DisplaySystem
					.getDisplaySystem();
			setCurrent();
			display.switchContext(this);
			GLContext.useContext(this);
			// Complete canvas initialization.
			Point size = this.getSize();
			display.initForCanvas(Math.max(size.x, 1), Math.max(size.y, 1));
			renderer = display.getRenderer();
			// Perform game initialization.
			if (impl instanceof JMECanvasImplementor2) {
				((JMECanvasImplementor2<LWJGLSWTCanvas>) impl).setCanvas(this);
			}
			impl.doSetup();

			// TODO Should this be moved into initForCanvas?
			if (DisplaySystem.getDisplaySystem().getMinSamples() != 0
					&& GLContext.getCapabilities().GL_ARB_multisample) {
				GL11.glEnable(ARBMultisample.GL_MULTISAMPLE_ARB);
			}
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Exception in initGL()", e);
		}

	}

	/**
	 * Called by the update thread.
	 */
	public void update() {
		long _currentts = System.currentTimeMillis();
		if (updateInput) {
			InputSystem.update();
		}
		impl.doUpdate();
		// calculate time for next render
		nextupdate = _currentts + updateMS;
	}

	/**
	 * Called by the render thread.
	 */
	public void render() {
		if (!isDisposed()) {
			long _currentts = System.currentTimeMillis();
			try {
				((LWJGLDisplaySystem) DisplaySystem.getDisplaySystem())
						.setRenderer(renderer);
				setCurrent();

				((LWJGLDisplaySystem) DisplaySystem.getDisplaySystem())
						.switchContext(this);
				GLContext.useContext(this);
				if (!drawWhenDirty || dirty) {

					impl.doRender();

					swapBuffers();

					dirty = false;
				}
			} catch (LWJGLException e) {
				logger.log(Level.SEVERE, "Exception in render()", e);
			}
			// calculate time for next render
			nextrender = _currentts + syncMS;
		}
	}

	/**
	 * Check if the input should be updated.
	 */
	public boolean isUpdateInput() {
		return updateInput;
	}

	/**
	 * Used to set whether the input should be updated.
	 * 
	 * @param doUpdate
	 *            true for updating, false for not.
	 */
	public void setUpdateInput(boolean doUpdate) {
		updateInput = doUpdate;
	}

	/**
	 * Set the targeted frame rate
	 * 
	 * @param fps
	 *            framse per second
	 */
	public void setTargetRate(int fps) {
		this.syncRate = fps;
		this.syncMS = 1000 / syncRate;
		if (single) {
			updateMS = syncMS;
		}
	}

	/**
	 * Set the amount of updates per second.
	 * 
	 * @param ups
	 *            updates per second
	 */
	public void setUpdateRate(int ups) {
		this.updateMS = 1000 / ups;
	}

    public void setAutoKillGfxContext(boolean shouldAutoKillGfxContext) {
        this.shouldAutoKillContext = shouldAutoKillGfxContext;
    }

	public int getTargetSyncRate() {
		return syncRate;
	}

	public void setDrawWhenDirty(boolean whenDirty) {
		this.drawWhenDirty = whenDirty;
	}

    public boolean shouldAutoKillGfxContext() {
        return shouldAutoKillContext;
    }

    public boolean isDrawWhenDirty() {
		return drawWhenDirty;
	}

    public void killGfxContext() {
        super.dispose();
        initialized = false;
    }

	public void makeDirty() {
		dirty = true;
	}

	/**
	 * @return the nextrender
	 */
	public long getNextrender() {
		return this.nextrender;
	}

	/**
	 * @return the nextupdate
	 */
	public long getNextupdate() {
		return this.nextupdate;
	}

	@Override
	public void setBounds(int x, int y, final int width, final int height) {
		super.setBounds(x, y, width, height);
		impl.resizeCanvas(width, height);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.swt.widgets.Control#setBounds(org.eclipse.swt.graphics.Rectangle
	 * )
	 */
	@Override
	public void setBounds(final Rectangle rect) {
		super.setBounds(rect);
		impl.resizeCanvas(rect.width, rect.height);
	}

}
